
import {groupBy, isFunction, mapObject} from "underscore";
import {whenEnabled} from "./util.js";
import {$} from "../util/seq.js";
import {ariaControlsOf} from "./selectors.js";

/**
 * @param buttons d3 selection of one or more button elements
 * @param {Function} handler the click event handler
 */
export function defineButton(buttons, handler) {
    buttons.on($`click`, whenEnabled(handler));
}

/**
 * Saves changes to a Model during click events.
 *
 * @param buttons d3 selection of one or more button elements
 * @param {Model} store the store to save changes to
 * @param {Function|*} changes the entries to save into the store, or ƒ(), where the result is the entry set to save
 */
export function defineModelButton(buttons, store, changes) {
    const fn = isFunction(changes) ? changes : () => changes;

    function updateStore() {
        store.save(fn(), {actor: this});
    }

    buttons.on($`click`, whenEnabled(updateStore));
}

/**
 * Sets a Handle with a specific value during click events.
 *
 * @param buttons d3 selection of one or more button elements
 * @param {Handle} handle the handle to set
 * @param {Function|*} value the value to store into the handle, or ƒ(v), where v is the current value and the result
 *        is the value to store
 */
export function defineHandleButton(buttons, handle, value) {
    const fn = isFunction(value) ? value : () => value;

    function updateStore() {
        handle.getAndSet(fn, {actor: this});
    }

    buttons.on($`click`, whenEnabled(updateStore));
}

/**
 * Implements WAI-ARIA Toggle Button pattern: https://www.w3.org/TR/wai-aria-practices/#button
 *
 * @param buttons d3 selection of one or more button elements that represent the same toggle feature
 * @param {Handle} handle the backing store to toggle during click events
 */
export function defineToggleButton(buttons, handle) {

    function updateDOM(value) {
        buttons.attr("aria-pressed", value === true);
    }

    function updateStore() {
        handle.toggle({actor: this});
    }

    updateDOM(handle.get());  // initialize
    handle.on($`change`, updateDOM);
    buttons.on($`click`, whenEnabled(updateStore));
}

/**
 * Implements WAI-ARIA Disclosure Button pattern: https://www.w3.org/TR/wai-aria-practices/#disclosure
 *
 * The elements referenced by the button's <i>aria-controls</i> attribute are hidden and unhidden accordingly.
 *
 * @param buttons d3 selection of one or more button elements that represent the same disclosure feature
 * @param {Handle} handle the backing store to toggle during click events
 */
export function defineDisclosureButton(buttons, handle) {
    const controls = ariaControlsOf(buttons.nodes());

    function updateDOM(value) {
        buttons.attr("aria-expanded", value === true);
        controls.property("hidden", value !== true);
    }

    function updateStore() {
        handle.toggle({actor: this});
    }

    updateDOM(handle.get());  // initialize
    handle.on($`change`, updateDOM);
    buttons.on($`click`, whenEnabled(updateStore));
}

/**
 * Implements WAI-ARIA Tab Button pattern: https://www.w3.org/TR/wai-aria-practices/#tabpanel
 * The behavior of the Tab List is implemented separately.
 *
 * The <i>data-name</i> attribute is saved to the backing store when a tab button is clicked.
 *
 * The tabpanels referenced by the buttons' <i>aria-controls</i> attributes are hidden and unhidden accordingly.
 *
 * @param buttons d3 selection of button elements in a tablist
 * @param {Handle} handle the backing store to update with the name of the clicked button
 * @param {Function?} fn optional callback ƒ(name, meta) to perform the store update, passing in the name of clicked
 *        tab and the meta object to use when mutating the store. When not specified, the default behavior is to
 *        store the name in the handle directly.
 */
export function defineTabButtons(buttons, handle, fn) {
    const nameToPanels = mapObject(groupBy(buttons.nodes(), node => node.dataset.name), ariaControlsOf);

    function updateDOM(value) {
        buttons.attr("aria-selected", function() { return value === this.dataset.name; });
        Object.entries(nameToPanels).forEach(([name, panels]) => panels.property("hidden", value !== name));
    }

    const updateStore = fn ?? ((name, meta) => handle.set(name, meta));

    function invokeStoreUpdater() {
        updateStore(this.dataset.name, {actor: this});
    }

    updateDOM(handle.get());  // initialize
    handle.on($`change`, updateDOM);
    buttons.on($`click`, whenEnabled(invokeStoreUpdater));
}

/**
 * Implements WAI-ARIA Radio Button pattern: https://www.w3.org/TR/wai-aria-practices/#radiobutton
 * The behavior of the Radio Group is implemented separately.
 *
 * The <i>data-name</i> attribute is saved to the backing store when the button is clicked.
 *
 * @param buttons d3 selection of button elements in a radiogroup
 * @param {Handle} handle the backing store to update with the name of the clicked button
 * @param {Function?} fn optional callback ƒ(name, meta) to perform the store update, passing in the name of clicked
 *        button and the meta object to use when mutating the store. When not specified, the default behavior is to
 *        store the name in the handle directly.
 */
export function defineRadioButtons(buttons, handle, fn) {

    function updateDOM(value) {
        buttons.attr("aria-checked", function() { return value === this.dataset.name; });
    }

    const updateStore = fn ?? ((name, meta) => handle.set(name, meta));

    function invokeStoreUpdater() {
        updateStore(this.dataset.name, {actor: this});
    }

    updateDOM(handle.get());  // initialize
    handle.on($`change`, updateDOM);
    buttons.on($`click`, whenEnabled(invokeStoreUpdater));
}
